/*
 * This file or a portion of this file is licensed under the terms of
 * the Globus Toolkit Public License, found in file GTPL, or at
 * http://www.globus.org/toolkit/download/license.html. This notice must
 * appear in redistributions of this file, with or without modification.
 *
 * Redistributions of this Software, with or without modification, must
 * reproduce the GTPL in: (1) the Software, or (2) the Documentation or
 * some other similar material which is provided with the Software (if
 * any).
 *
 * Copyright 1999-2004 University of Chicago and The University of
 * Southern California. All rights reserved.
 */
package edu.isi.pegasus.planner.invocation;

import java.net.*;
import java.io.*;
import java.sql.SQLException;
import java.util.Iterator;

import edu.isi.pegasus.planner.parser.InvocationParser;
import org.griphyn.vdl.toolkit.*;
import org.griphyn.vdl.dbschema.*;
import org.griphyn.vdl.util.Logging;
import org.griphyn.vdl.util.ChimeraProperties;
import org.griphyn.vdl.directive.*;

import org.apache.log4j.Logger;
import org.apache.log4j.Level;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.PatternLayout;

public class SimpleServer extends Toolkit
{
  private static final int port = 65533;

  public static boolean c_terminate = false;
  public static Logger c_logger = null;

  private boolean m_emptyFail = true;
  private boolean m_noDBase;

  DatabaseSchema m_dbschema;
  InvocationParser m_parser;
  ServerSocket m_server;

  public static void setTerminate( boolean b )
  {
    c_terminate = b;
  }

  public static boolean getTerminate()
  {
    return c_terminate;
  }

  public void showUsage() 
  {
    // empty for now
  }

  public SimpleServer( int port )
    throws Exception
  {
    super("SimpleServer");

    // stand up the connection to the PTC 
    this.m_noDBase = false;
    ChimeraProperties props = ChimeraProperties.instance();
    String ptcSchemaName = props.getPTCSchemaName();
    if ( ptcSchemaName == null ) m_noDBase = true;
    if ( ! m_noDBase ) {
      // ignore -d option for now - grumbl, why?!
      Connect connect = new Connect();
      this.m_dbschema = connect.connectDatabase(ptcSchemaName);

      // check for invocation record support
      if ( ! (m_dbschema instanceof PTC) ) {
	c_logger.warn( "Your database cannot store invocation records" + 
		       ", assuming no-database-mode" );
	m_noDBase = true;
      }
    }

    // create one XML parser -- once
    m_parser = new InvocationParser( props.getPTCSchemaLocation() );


    // setup socket
    this.m_server = null;
    try {
      byte[] loopback = { 127, 0, 0, 1 };
      this.m_server =
	new ServerSocket( port, 5, InetAddress.getByAddress(loopback) );
//	new ServerSocket( port, 5, InetAddress..getLocalHost() );
//	new ServerSocket( port, 5 );
    } catch ( UnknownHostException e ) {
      c_logger.fatal( "Unable to determine own hostname: " +
		      e.getMessage() );
      System.exit(1);
    } catch ( IOException e ) {
      c_logger.fatal( "Could not listen on port " + port + ": " + 
		      e.getMessage() );
      System.exit(1);
    }
  }

  /**
   * Copy the content of the file into memory. The copy operation also
   * weeds out anything that may have been added by the remote batch
   * scheduler. For instance, PBS is prone to add headers and footers.
   *
   * @param input is the file instance from which to read contents.
   * @return the result code from reading the file
   */
  private String extractToMemory( java.io.File input )
    throws FriendlyNudge
  {
    StringWriter out = null;

    // open the files
    int p1, p2, state = 0; 
    try {
      BufferedReader in = new BufferedReader( new FileReader(input) );
      out = new StringWriter(4096);

      String line = null;
      while ( (line = in.readLine()) != null ) {
	if ( (state & 1) == 0 ) {
	  // try to copy the XML line in any case
	  if ( (p1 = line.indexOf( "<?xml" )) > -1 ) 
	    if ( (p2 = line.indexOf( "?>", p1 )) > -1 )
	      out.write( line, p1, p2+2 );
	  // start state with the correct root element 
	  if ( (p1 = line.indexOf( "<invocation")) > -1 ) {
	    if ( p1 > 0 ) line = line.substring( p1 );
	    ++state;
	  }    
	}
	if ( (state & 1) == 1 ) {
	  out.write( line );
	  if ( (p1 = line.indexOf("</invocation>")) > -1 ) ++state;
	}
      }

      in.close();
      out.flush();
      out.close();
    } catch ( IOException ioe ) {
      throw new FriendlyNudge( "While copying " + input.getPath() + 
			       " into temp. file: " + ioe.getMessage(), 5 );
    }

    // some sanity checks
    if ( state == 0 )
      throw new FriendlyNudge( "File " + input.getPath() + 
			       " does not contain invocation records, assuming failure", 5 );
    if ( (state & 1) == 1 )
      throw new FriendlyNudge( "File " + input.getPath() +
			       " contains an incomplete invocation record, assuming failure", 5 );

    // done
    return out.toString();
  }


  /**
   * Determines the exit code of an invocation record. Currently,
   * we will determine the exit code from the main job only.
   *
   * @param ivr is the invocation record to put into the database
   * @return the status code as exit code to signal failure etc. 
   * <pre>
   *   0   regular exit with exit code 0
   *   1   regular exit with exit code > 0
   *   2   failure to run program from kickstart
   *   3   application had died on signal
   *   4   application was suspended (should not happen)
   *   5   failure in exit code parsing
   *   6   impossible case
   * </pre>
   */
  private int determineExitStatus( InvocationRecord ivr )
  {
    boolean seen = false;
    for ( Iterator i=ivr.iterateJob(); i.hasNext(); ) {
      Job job = (Job) i.next();

      // clean-up jobs don't count in failure modes
      if ( job.getTag().equals("cleanup") ) continue;

      // obtains status from job
      Status status = job.getStatus(); 
      if ( status == null ) return 6;

      JobStatus js = status.getJobStatus();
      if ( js == null ) {
	// should not happen
	return 6;
      } else if ( js instanceof JobStatusRegular ) {
	// regular exit code - success or failure?
	int exitcode =  ((JobStatusRegular) js).getExitCode();
	if ( exitcode != 0 ) return 1;
	else seen = true;
	// continue, if exitcode of 0 to implement chaining !!!!
      } else if ( js instanceof JobStatusFailure ) {
	// kickstart failure
	return 2;
      } else if ( js instanceof JobStatusSignal ) {
	// died on signal
	return 3;
      } else if ( js instanceof JobStatusSuspend ) {
	// suspended???
	return 4;
      } else {
	// impossible/unknown case
	return 6;
      }
    }

    // success, or no [matching] jobs
    return seen ? 0 : 5;
  }

  /**
   * Reads the contents of the specified file, and returns with the
   * remote exit code contained in the job chain.
   *
   * @param filename is the name of the file with the kickstart record.
   * @return the exit code derived from the remote exit code. 
   */
  public int checkFile( String filename )
  {
    int result = 0;

    try {
      // check input file
      java.io.File check = new java.io.File(filename);

      // test 1: file exists
      if ( ! check.exists() )
	throw new FriendlyNudge( "file does not exist " + filename +
				 ", assuming failure", 5 );

      // test 2: file is readable
      if ( ! check.canRead() )
	throw new FriendlyNudge( "unable to read file " + filename +
				 ", assuming failure", 5 );

      // test 3: file has nonzero size
      if ( check.length() == 0 ) {
	if ( m_emptyFail ) {
	  throw new FriendlyNudge( "file " + filename + " has zero length" +
				   ", assuming failure", 5 );
	} else {
	  throw new FriendlyNudge( "file " + filename + " has zero length" +
				   ", assuming success", 0 );
	}
      }

      // test 4: extract XML into tmp file
      String temp = extractToMemory(check);

      // test 5: try to parse XML -- but there is only one parser
      InvocationRecord invocation = null;
      synchronized ( m_parser ) {
	c_logger.info( "starting to parse invocation" );
	invocation = m_parser.parse( new StringReader(temp) );
      };

      if ( invocation == null )
	throw new FriendlyNudge( "invalid XML invocation record in " +
				 filename + ", assuming failure", 5 );
      else 
	c_logger.info( "invocation was parsed successfully" );

      // insert into database. This trickery works, because we already
      // checked previously that the dbschema does support invocations. 
      // However, there is only one database connection at a time. 
      if ( ! m_noDBase ) {
	PTC ptc = (PTC) m_dbschema;
	
	synchronized ( ptc ) {
	  // FIXME: (start,host,pid) may not be a sufficient secondary key
	  if ( ptc.getInvocationID( invocation.getStart(),
				    invocation.getHostAddress(), 
				    invocation.getPID() ) == -1 ) {
	    c_logger.info( "adding invocation to database" );
	    // may throw SQLException
	    ptc.saveInvocation( invocation );
	  } else {
	    c_logger.info( "invocation already exists, skipping!" );
	  }
	}
      }
      
      // determine result code, just look at the main job for now
      c_logger.info( "determining exit status of main job" );
      result = determineExitStatus( invocation );
      c_logger.info( "exit status = " + result );
      
    } catch ( FriendlyNudge fn ) {
      c_logger.warn( fn.getMessage() );
      result = fn.getResult();
      
    } catch ( Exception e ) {
      c_logger.warn( e.getMessage() );
      result = 5;
    }

    // done
    return result;
  }

  public static void main( String args[] )
    throws IOException
  {
    // setup logging
    System.setProperty( "log4j.defaultInitOverride", "true" );
    Logger root = Logger.getRootLogger();
    root.addAppender(	
	new ConsoleAppender(
	new PatternLayout("%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%c{1}] %m%n") ) );
    root.setLevel( Level.INFO );
    c_logger = Logger.getLogger( SimpleServer.class );
    c_logger.info( "starting" );

    SimpleServer me = null;
    try {
      me = new SimpleServer(port);
    } catch ( Exception e ) {
      c_logger.fatal( "Unable to instantiate a server: " + e.getMessage() );
      System.exit(1);
    }

    // run forever
    try {
      while ( ! c_terminate ) { 
	new SimpleServerThread( me, me.m_server.accept() ).start();
      }
    } catch ( SocketException se ) {
      // ignore -- closing the server socket in a thread during shutdown
      // will have accept fail with a socket exception in main()
    }

    // done
    c_logger.info( "received shutdown" );

    // count your threads, and the last one locks the
    // door and switches off the light... Grrr. 
    synchronized ( me ) {
      while ( SimpleServerThread.c_count > SimpleServerThread.c_cdone ) {
	try {
	  me.wait(5000);
	} catch ( InterruptedException e ) {
	  // ignore 
	}
      }
    }

    try { 
      me.m_dbschema.close(); 
    } catch ( Exception e ) {
      c_logger.warn( "During database disconnect: " + e.getMessage() );
    }
    c_logger.warn( "finished shutdown" );
  }
}
